#include "gcm_mode.h"
#include "../../common/misc.h"
#include "../../common/net/socket.h"
#include "../../common/serialize/serialize.h"

static BYTE g_bfZero[64] = {0};

C_GCM_MODE::C_GCM_MODE()
{
    InitG();
    bzero(&m_bfY, 16);
    m_bufCache.reserve(64);
}

int C_GCM_MODE::Init(BYTE *pIV, int iIV, BYTE *pAAD, int iAAD, IBlockCipherKernel *pobjKer)
{
    int iResult, iTmp;
    int iBlock = pobjKer->GetBlockLen();

    iResult = pobjKer->Encrypt(g_bfZero, m_bfH); // 计算H

    bzero(&m_bfY, 16);
    m_bufCache.clear();

    m_pobjKer = pobjKer;
    // IV//[
    if(iIV == 12)
    {
        memcpy(m_bfY, pIV, 12);
        m_bfY[12] = m_bfY[13] = m_bfY[14] = 0;
        m_bfY[15]                         = 1;
    }
    else
    {
        InitG();
        iResult = UpdateG(pIV, iIV);
        iResult = FinishG(m_bfY);
    }
    iResult = pobjKer->Encrypt(m_bfY, m_bfY0);
    //]
    InitG(pAAD, iAAD);

    return 0;
}

static void Xor16(BYTE *pIn1, BYTE *pIn2, BYTE *pOut)
{
    for(int i = 0; i < 16; i++) pOut[i] = pIn1[i] ^ pIn2[i];
    return;
}

static int ShiftRight16(BYTE *data)
{
    U8 u8Curr = 0, u8Last = 0;

    for(int i = 0; i < 16; i++)
    {
        u8Curr  = data[i] << 7;
        data[i] = ((data[i] >> 1) | u8Last);
        u8Last  = u8Curr;
    }

    return u8Last != 0;
}

static void GCMul(BYTE *pU, BYTE *pV, BYTE *pW)
{ // W=U*V
    int iResult;
    static BYTE s_bfR[16] = {0xE1, 0};
    BYTE bfZ[16];
    U8 vi;

    bzero(pW, 16);
    memcpy(bfZ, pU, 16);

    for(int i = 0; i < 128; i++)
    {
        vi = (pV[i / 8] >> (7 - (i % 8))) & 1;
        if(vi) Xor16(pW, bfZ, pW);
        iResult = ShiftRight16(bfZ);
        if(iResult) Xor16(bfZ, s_bfR, bfZ);
    }

    return;
}

int C_GCM_MODE::InitG(BYTE *data, int iLen)
{
    int iRead = 0;
    bzero(m_bfX, 16);
    m_u64Z = m_u64W = 0;

    if(data != nullptr)
    {
        while(iLen - iRead >= 16)
        {
            UpdateG(data + iRead, 16, 1);
            iRead += 16;
        }
        if(iLen - iRead > 0)
        {
            UpdateG(data + iRead, iLen - iRead, 1);
            iRead = iLen;
        }
    }

    return 0;
}

int C_GCM_MODE::UpdateG(BYTE *data, int iLen, int bW)
{
    BYTE bfPad[16], bfTmp[16];

    if(data == nullptr || iLen <= 0 || iLen > 16) return COMMON_ERR_INV_ARGS;

    bzero(bfPad, 16);
    memcpy(bfPad, data, iLen);
    Xor16(m_bfX, bfPad, bfTmp);
    GCMul(bfTmp, m_bfH, m_bfX);

    if(bW) m_u64W += (iLen * 8);
    else m_u64Z += (iLen * 8);
    return 0;
}

int C_GCM_MODE::FinishG(OUT BYTE *data)
{
    BYTE bfX[16], bfTmp[16];

    SAPI_U64ToBytes(m_u64W, bfTmp);
    SAPI_U64ToBytes(m_u64Z, bfTmp + 8);
    Xor16(m_bfX, bfTmp, bfX);
    GCMul(bfX, m_bfH, data);

    return 0;
}

int C_GCM_MODE::Encrypt(BYTE *pIn, int iLenIn, BYTE *pOut, int *piLenOut)
{
    int iRet = -1;
    int iResult, iTmp;
    int iRead = 0, iWrite = 0;
    BYTE bfTmp[16];

    if(pIn == nullptr)
    {
        if(m_bufCache.size() > 0)
        {
            IncY();
            iResult = m_pobjKer->Encrypt(m_bfY, bfTmp);
            for(int i = 0; i < m_bufCache.size(); i++) pOut[i] = bfTmp[i] ^ m_bufCache[i];
            iWrite += m_bufCache.size();
            m_bufCache.clear();

            iResult = UpdateG(pOut, iWrite);
        }

        iResult = FinishG(bfTmp);
        Xor16(bfTmp, m_bfY0, pOut + iWrite);
        iWrite += 16;
        SET_RET_AND_GOTO_EXIT(0);
    }

    if(m_bufCache.size() > 0)
    {
        while(m_bufCache.size() < 16 && iRead < iLenIn)
        {
            m_bufCache.push_back(pIn[iRead]);
            iRead++;
        }
        if(m_bufCache.size() >= 16)
        {
            iResult = Encrypt(m_bufCache.data(), pOut + iWrite);
            iWrite += 16;
            m_bufCache.clear();
        }
    }
    if(iRead >= iLenIn) SET_RET_AND_GOTO_EXIT(0);

    while(iRead + 16 <= iLenIn)
    {
        iResult = Encrypt(pIn + iRead, pOut + iWrite);
        iRead += 16;
        iWrite += 16;
    }

    if(iRead < iLenIn) STD_ASSIGN(m_bufCache, pIn + iRead, iLenIn - iRead);
    iRet = 0;
_Exit:
    if(iRet >= 0) *piLenOut = iWrite;
    return iRet;
}

int C_GCM_MODE::Decrypt(BYTE *pIn, int iLenIn, BYTE *pOut, int *piLenOut)
{
    int iRet = -1;
    int iResult, iTmp;
    int iRead = 0, iWrite = 0;
    BYTE bfTmp[16];

    if(pIn == nullptr)
    {
        if(m_bufCache.size() > 0)
        {
            iResult = UpdateG(m_bufCache.data(), m_bufCache.size());

            IncY();
            iResult = m_pobjKer->Encrypt(m_bfY, bfTmp);
            for(int i = 0; i < m_bufCache.size(); i++) pOut[i] = bfTmp[i] ^ m_bufCache[i];
            iWrite += m_bufCache.size();
            m_bufCache.clear();
        }

        iResult = FinishG(bfTmp);
        Xor16(bfTmp, m_bfY0, pOut + iWrite);
        iWrite += 16;
        SET_RET_AND_GOTO_EXIT(0);
    }

    if(m_bufCache.size() > 0)
    {
        while(m_bufCache.size() < 16 && iRead < iLenIn)
        {
            m_bufCache.push_back(pIn[iRead]);
            iRead++;
        }
        if(m_bufCache.size() >= 16)
        {
            iResult = Decrypt(m_bufCache.data(), pOut + iWrite);
            iWrite += 16;
            m_bufCache.clear();
        }
    }
    if(iRead >= iLenIn) SET_RET_AND_GOTO_EXIT(0);

    while(iRead + 16 <= iLenIn)
    {
        iResult = Decrypt(pIn + iRead, pOut + iWrite);
        iRead += 16;
        iWrite += 16;
    }

    if(iRead < iLenIn) STD_ASSIGN(m_bufCache, pIn + iRead, iLenIn - iRead);
    iRet = 0;
_Exit:
    if(iRet >= 0) *piLenOut = iWrite;
    return iRet;
}

int C_GCM_MODE::Encrypt(BYTE *pIn, BYTE *pOut)
{
    int iRet = -1;
    int iResult, iTmp;

    IncY();
    iResult = m_pobjKer->Encrypt(m_bfY, pOut);
    Xor16(pIn, pOut, pOut);
    iResult = UpdateG(pOut, 16);

    iRet = 0;
_Exit:
    return iRet;
}

int C_GCM_MODE::Decrypt(BYTE *pIn, BYTE *pOut)
{
    int iRet = -1;
    int iResult, iTmp;

    IncY();
    iResult = m_pobjKer->Encrypt(m_bfY, pOut);
    Xor16(pIn, pOut, pOut);
    iResult = UpdateG(pIn, 16);

    iRet = 0;
_Exit:
    return iRet;
}

int C_GCM_MODE::IncY()
{
    U32 *p = (U32 *)(&m_bfY[12]);
    *p     = htonl(ntohl(*p) + 1);
    return 0;
}
